﻿using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace Sidvall.Reflection
{
    public class TypeCache
    {
        #region Public Members

        #region CodeManager

        private Sidvall.Reflection.CodeManager _CodeManager;
        public Sidvall.Reflection.CodeManager CodeManager
        {
            get
            {
                if (_CodeManager != null)
                    return _CodeManager;
                return Sidvall.SystemContext.Current.CodeManager;
            }
            set
            {
                _CodeManager = value;
            }
        }

        #endregion
        #region StorageManager

        private Sidvall.IO.StorageManager _StorageManager;
        public Sidvall.IO.StorageManager StorageManager
        {
            get
            {
                if (_StorageManager != null)
                    return _StorageManager;
                return Sidvall.SystemContext.Current.SystemStorageManager;
            }
            set
            {
                _StorageManager = value;
            }
        }

        #endregion
        #region Assemblies

        public IEnumerable<Assembly> Assemblies
        {
            get
            {
                return (from o in this.AssemblyIndex.Values
                        select o.Assembly);
            }
        }

        #endregion
        #region Types

        public IEnumerable<System.Type> Types
        {
            get
            {
                return this.TypeIndex.Values;
            }
        }

        #endregion

        #region AddAssembly

        public void AddAssembly(Assembly assembly)
        {
            AddAssembly(assembly, null);
        }
        public void AddAssembly(Assembly assembly, string sourceFilePath)
        {
            AssemblyItem item;

            if (assembly == null)
                return;
            item = new AssemblyItem()
            {
                Assembly = assembly,
                SourceFilePath = sourceFilePath,
            };
            AddAssembly(item);
        }
        public void AddAssembly(AssemblyItem item)
        {
            string name;

            if (item == null)
                return;
            if (item.Assembly == null)
                return;
            name = item.Assembly.GetName().Name;
            if (!this.AssemblyIndex.ContainsKey(name))
            {
                lock (_mutex)
                {
                    if (!this.AssemblyIndex.ContainsKey(name))
                    {
                        this.AssemblyIndex.Add(name, item);
                    }
                }
            }
        }

        #endregion
        #region AddAssemblies

        public void AddAssemblies(IEnumerable<Assembly> assemblies)
        {
            if (assemblies == null)
                return;
            foreach (var assembly in assemblies)
                AddAssembly(assembly);
        }

        #endregion
        #region AddAssembliesAsync

        public async Task AddAssembliesAsync(string folderPath)
        {
            await AddAssembliesAsync(folderPath, null, IO.SearchOption.AllDirectories).ConfigureAwait(false);
        }
        public async Task AddAssembliesAsync(string folderPath, IEnumerable<string> searchPatterns, Sidvall.IO.SearchOption searchOption)
        {
            if (string.IsNullOrWhiteSpace(folderPath))
            {
                this.CurrentFolderPath = string.Empty;
                return;
            }
            if (this.CurrentFolderPath.Equals(folderPath, System.StringComparison.OrdinalIgnoreCase))
                return;
            if (!await this.StorageManager.FolderExistsAsync(folderPath).ConfigureAwait(false))
                return;

            this.CurrentFolderPath = folderPath;
            if (searchPatterns == null)
                searchPatterns = new string[] { "*.dll", "*.exe" };
            var fileNames = await this.StorageManager.GetFileNamesAsync(folderPath, searchPatterns, searchOption).ConfigureAwait(false);
            await AddAssembliesAsync(fileNames).ConfigureAwait(false);
        }
        public async Task AddAssembliesAsync(IEnumerable<string> fileNames)
        {
            if (fileNames == null)
                return;
            foreach (var fileName in fileNames)
            {
                var assembly = GetAssemblyFromSourceFilePath(fileName);
                if (assembly != null)
                    continue;
                assembly = await this.CodeManager.GetAssemblyAsync(fileName).ConfigureAwait(false);
                AddAssembly(assembly, fileName);
            }
        }

        #endregion
        #region AddType

        public void AddType(string typeName, System.Type type)
        {
            if (string.IsNullOrWhiteSpace(typeName))
                return;
            if (type == null)
                return;
            if (!this.TypeIndex.ContainsKey(typeName))
            {
                lock (_mutex)
                {
                    if (!this.TypeIndex.ContainsKey(typeName))
                    {
                        this.TypeIndex.Add(typeName, type);
                    }
                }
            }
        }

        #endregion
        #region GetAssemblyFromName

        public Assembly GetAssemblyFromName(string name)
        {
            if (string.IsNullOrWhiteSpace(name))
                return null;
            return (from o in this.AssemblyIndex
                    where o.Value.Assembly.GetName().Name.Equals(name, System.StringComparison.OrdinalIgnoreCase)
                    select o.Value.Assembly).FirstOrDefault();
        }

        #endregion
        #region GetAssemblyFromSourceFilePath

        public Assembly GetAssemblyFromSourceFilePath(string sourceFilePath)
        {
            if (string.IsNullOrWhiteSpace(sourceFilePath))
                return null;
            return (from o in this.AssemblyIndex
                    where o.Value.SourceFilePath.Equals(sourceFilePath, System.StringComparison.OrdinalIgnoreCase)
                    select o.Value.Assembly).FirstOrDefault();
        }

        #endregion
        #region GetType

        public System.Type GetType(string typeName)
        {
            System.Type type;

            if (string.IsNullOrWhiteSpace(typeName))
                return null;
            if (this.TypeIndex.TryGetValue(typeName, out type))
                return type;
            type = this.CodeManager.GetType(typeName);
            if ((type == null) && (this.Assemblies != null) && (this.Assemblies.Any()))
                type = this.CodeManager.GetType(typeName, this.Assemblies);
            AddType(typeName, type);

            return type;
        }

        #endregion
        #region GetTypeFromCache

        public System.Type GetTypeFromCache(string typeName)
        {
            System.Type type;

            if (string.IsNullOrWhiteSpace(typeName))
                return null;
            if (this.TypeIndex.TryGetValue(typeName, out type))
                return type;
            return null;
        }

        #endregion
        #region ToString

        public override string ToString()
        {
            return "TypeCache - Dictionary";
        }

        #endregion

        #endregion
        #region Factory Members

        #region NewTypeCacheAsync

        public static async Task<TypeCache> NewTypeCacheAsync(string folderPath)
        {
            var item = new TypeCache();
            await item.AddAssembliesAsync(folderPath).ConfigureAwait(false);
            return item;
        }
        public static async Task<TypeCache> NewTypeCacheAsync(string folderPath, IEnumerable<string> searchPatterns, Sidvall.IO.SearchOption searchOption)
        {
            var item = new TypeCache();
            await item.AddAssembliesAsync(folderPath, searchPatterns, searchOption).ConfigureAwait(false);
            return item;
        }

        #endregion

        #endregion
        #region Private Members

        private readonly object _mutex = new object();

        #region CurrentFolderPath

        private string _CurrentFolderPath = string.Empty;
        private string CurrentFolderPath
        {
            get
            {
                return _CurrentFolderPath;
            }
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                    value = string.Empty;
                _CurrentFolderPath = value;
            }
        }

        #endregion
        #region AssemblyIndex

        private System.Lazy<Dictionary<string, AssemblyItem>> _AssemblyIndex = new System.Lazy<Dictionary<string, AssemblyItem>>(GetAssemblyIndex);
        private Dictionary<string, AssemblyItem> AssemblyIndex
        {
            get
            {
                return _AssemblyIndex.Value;
            }
        }

        #endregion
        #region TypeIndex

        private System.Lazy<Dictionary<string, System.Type>> _TypeIndex = new System.Lazy<Dictionary<string, System.Type>>(GetTypeIndex);
        private Dictionary<string, System.Type> TypeIndex
        {
            get
            {
                return _TypeIndex.Value;
            }
        }

        #endregion

        #region GetAssemblyIndex

        private static Dictionary<string, AssemblyItem> GetAssemblyIndex()
        {
            return new Dictionary<string, AssemblyItem>(System.StringComparer.CurrentCultureIgnoreCase);
        }

        #endregion
        #region GetTypeIndex

        private static Dictionary<string, System.Type> GetTypeIndex()
        {
            return new Dictionary<string, System.Type>(System.StringComparer.CurrentCultureIgnoreCase);
        }

        #endregion

        #endregion
        #region Constructors

        public TypeCache()
        {
        }
        public TypeCache(IEnumerable<Assembly> assemblies)
        {
            AddAssemblies(assemblies);
        }

        #endregion
    }
}
